﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using ComponentBuilder;

namespace BlamanticUI.Components.Forms;

/// <summary>
/// A select list that contains search input box.
/// </summary>
/// <typeparam name="TItem">The item type of data source.</typeparam>
/// <typeparam name="TValue">The type of value for two-way binding and search.</typeparam>
public class SelectList<TItem, TValue> : BlazorInputComponentBase<TValue>, IDisposable,
    IHasUIComponent,
    IHasSelectable,
    IHasOnActive,
    IHasFluid,
    IHasSize,
    IHasCompact,
    IHasInverted,
    IHasSpan,
    IHasInline,
    IHasDisabled
{

    /// <summary>
    /// Initializes a new instance of the <see cref="SelectList{TItem,TValue}"/> class.
    /// </summary>
    public SelectList()
    {
       // Filterable = true;
        Selectable = true;
        LocalFilter = DefaultFilter;
        EmptyContent = builder => builder.AddContent(0, "No results found.");
        ItemFormatContent = value => new RenderFragment(builder => builder.AddContent(0, value?.ToString()));
        LoadingContent = builder => builder.CreateComponent<Loader>(0, attributes: new { Inverted });
        DataSource = Enumerable.Empty<TItem>();
        DataSourceHandler = value => Task.FromResult(DataSource);
    }

    #region Parameters        
    /// <summary>
    /// Gets or sets a value indicating whether hover style while cursor moving over items.
    /// </summary>
    [Parameter][CssClass("selection")] public bool Selectable { get; set; }

    /// <summary>
    /// Gets or sets to display a input filter for candidate items.
    /// </summary>
    [Parameter][CssClass("search")] public bool Filterable { get; set; }
    /// <summary>
    /// Sets drop down items can be scrollable.
    /// </summary>
    [Parameter][CssClass("scrolling")]public bool Scrollable { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether the drop up direction.
    /// </summary>
    [Parameter][CssClass("upward")] public bool Up { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether this is fluid.
    /// </summary>
    /// <value>
    ///   <c>true</c> if fluid; otherwise, <c>false</c>.
    /// </value>
    [Parameter] public bool Fluid { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether this drop down list is actived.
    /// </summary>
    /// <value>
    ///   <c>true</c> if actived; otherwise, <c>false</c>.
    /// </value>
    [Parameter][CssClass("active")] public bool Active { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether the selected item can be removable.
    /// </summary>
    /// <value>
    ///   <c>true</c> if removable; otherwise, <c>false</c>.
    /// </value>
    [Parameter] public bool Removable { get; set; }

    /// <summary>
    /// Gets or sets the size.
    /// </summary>
    [Parameter] public Size? Size { get; set; }
    /// <summary>
    /// Gets or sets a value indicating whether to compact space of text.
    /// </summary>
    /// <value>
    ///   <c>true</c> if compact; otherwise, <c>false</c>.
    /// </value>
    [Parameter] public bool Compact { get; set; }

    /// <summary>
    /// Gets or sets the maximum items in dropdown list. Default is 10.
    /// </summary>
    [Parameter] public int MaxItems { get; set; } = 10;
    /// <summary>
    /// Gets or sets the span of column.
    /// </summary>
    [Parameter][CssClass(" column", Suffix = true)] public SpanSize? Span { get; set; }
    /// <summary>
    /// Gets or sets a value indicating whether is inline paragraph.
    /// </summary>
    /// <value>
    ///   <c>true</c> if inline; otherwise, <c>false</c>.
    /// </value>
    [Parameter] public bool Inline { get; set; }
    /// <summary>
    /// 
    /// </summary>
    [Parameter] public bool Inverted { get; set; }
    /// <summary>
    /// Gets or sets a value indicating whether is readonly withou user interaction.
    /// </summary>
    [Parameter][CssClass("read-only")]public bool ReadOnly { get; set; }

    /// <summary>
    /// Gets or sets the default item before selected.
    /// </summary>
    [Parameter] public RenderFragment DefaultItemContent { get; set; }
    /// <summary>
    /// Gets or sets the format of item to display in list.
    /// </summary>
    [Parameter] public RenderFragment<TItem> ItemFormatContent { get; set; }

    /// <summary>
    /// Gets or sets the UI content while empty filtered items from data source.
    /// </summary>
    [Parameter] public RenderFragment EmptyContent { get; set; }
    /// <summary>
    /// Gets or sets the UI content when data is loading to fill data source.
    /// </summary>
    [Parameter]public RenderFragment LoadingContent { get; set; }

    /// <summary>
    /// Customization a filter for existing data.
    /// <list type="bullet">
    /// <item>arg1: the text of search input;</item>
    /// <item>arg2: a item of data source;</item>
    /// <item>return <c>true</c> if result matched for filter, otherwise <c>false</c>.</item>
    /// </list>
    /// </summary>
    [Parameter] public Func<string, TItem, bool> LocalFilter { get; set; }


    /// <summary>
    /// The data source of dropdown items.
    /// </summary>
    [Parameter] public IEnumerable<TItem>? DataSource { get; set; }

    /// <summary>
    /// A function that returns data source of dropdown items.
    /// </summary>
    [Parameter] public Func<string?, Task<IEnumerable<TItem>>> DataSourceHandler { get; set; }

    /// <summary>
    /// The field name of data source when value is changed.
    /// </summary>
#if NET6_0_OR_GREATER
[EditorRequired]
#endif
    [Parameter]public string ValueField { get; set; }
    /// <summary>
    /// Gets or sets a callback method whether active state has changed.
    /// </summary>
    [Parameter] public EventCallback<bool> OnActive { get; set; }
    /// <summary>
    /// A callback method whether an item is selected.
    /// </summary>
    [Parameter]public EventCallback<TItem> OnItemSelected { get; set; }
    /// <summary>
    /// Disabled component to use.
    /// </summary>
    [Parameter] public bool Disabled { get; set; }

#endregion

#region Protected
    /// <summary>
    /// Gets the filtered data source.
    /// </summary>
    protected IEnumerable<TItem> FilterdDataSource { get; private set; }

    /// <summary>
    /// Gets a value indicating whether display in drop down list that has filtered items.
    /// </summary>
    protected bool Filtered { get; private set; }

    /// <summary>
    /// Gets the filterd value of search input.
    /// </summary>
    protected string? FilterdInputValue { get; private set; }
#endregion

    /// <summary>
    /// Gets the selected item.
    /// </summary>
    public TItem SelectedItem { get;private set; }

    /// <summary>
    /// Gets a loading state wheither the dropdown items are loading.
    /// </summary>
    public bool IsLoading { get; private set; }

    protected override void OnParametersSet()
    {
        if (string.IsNullOrEmpty(ValueField))
        {
            throw new ArgumentException($"Parameter {nameof(ValueField)} is required");
        }
    }

    /// <summary>
    /// Method invoked after each time the component has been rendered. Note that the component does
    /// not automatically re-render after the completion of any returned <see cref="T:System.Threading.Tasks.Task" />, because
    /// that would cause an infinite render loop.
    /// </summary>
    /// <param name="firstRender">Set to <c>true</c> if this is the first time <see cref="M:Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean)" /> has been invoked
    /// on this component instance; otherwise <c>false</c>.</param>
    /// <remarks>
    /// The <see cref="M:Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean)" /> and <see cref="M:Microsoft.AspNetCore.Components.ComponentBase.OnAfterRenderAsync(System.Boolean)" /> lifecycle methods
    /// are useful for performing interop, or interacting with values recieved from <c>@ref</c>.
    /// Use the <paramref name="firstRender" /> parameter to ensure that initialization work is only performed
    /// once.
    /// </remarks>
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await Search(default);
        }
    }

    protected override void BuildCssClass(ICssClassBuilder builder)
    {
        builder.Append("loading",IsLoading).Append("visible", Active).Append("clearable", Removable).Append("dropdown");
    }

    protected override void AddContent(RenderTreeBuilder builder, int sequence)
    {
        BuildLoadingContent(builder, 0);
        BuildDefaultText(builder, 1);
        BuildDropdownIcon(builder, 2);
        BuildRemoveIcon(builder, 3);
        BuildSearchInput(builder, 4);
        BuildDropDownItems(builder, 5);
    }

    protected override void BuildAttributes(IDictionary<string, object> attributes)
    {
        if (!Disabled)
        {
            attributes["tabindex"] = new Random().Next();
            attributes["onclick"] = EventCallback.Factory.Create<MouseEventArgs>(this, Toggle);
            attributes["onblur"] = EventCallback.Factory.Create<FocusEventArgs>(this, () => this.Activate(false));

        }
    }

#region BuildElement
    /// <summary>
    /// Builds the remove icon.
    /// </summary>
    /// <param name="builder">The child.</param>
    private void BuildRemoveIcon(RenderTreeBuilder builder,int sequence)
    {
        if (IsLoading)
        {
            return;
        }
        builder.CreateComponent<Icon>(sequence, attributes: new
        {
            IconClass = "remove",
            onclick = EventCallback.Factory.Create<MouseEventArgs>(this, () => SelectItem(default))
        }, appendFunc: (b, s) =>
        {
            b.AddEventStopPropagationAttribute(s, "onclick", true);
            return s + 1;
        },
        condition: Removable && SelectedItem != null);
    }

    /// <summary>
    /// Builds the search input.
    /// </summary>
    /// <param name="builder">The builder.</param>
    private void BuildSearchInput(RenderTreeBuilder builder,int sequence)
    {
        if (IsLoading)
        {
            return;
        }
        builder.CreateElement(sequence, "input", attributes: new
        {
            @class = "search",
            value= FilterdInputValue,
            oninput = EventCallback.Factory.Create<ChangeEventArgs>(this, e => Search(e.Value?.ToString())),
            onblur= EventCallback.Factory.Create<FocusEventArgs>(this, () => this.Activate(false))
        },condition:Filterable);
    }

    /// <summary>
    /// Builds the dropdown icon.
    /// </summary>
    /// <param name="builder">The builder.</param>
    private static void BuildDropdownIcon(RenderTreeBuilder builder, int sequence)
    {
        builder.CreateComponent<Icon>(sequence, attributes: new { IconClass = "dropdown" });
    }

    /// <summary>
    /// Builds the default text.
    /// </summary>
    /// <param name="builder">The child.</param>
    private void BuildDefaultText(RenderTreeBuilder builder,int sequence)
    {
        if (IsLoading)
        {
            return;
        }
        builder.CreateElement(sequence, "div", content =>
        {

            if (SelectedItem != null)
            {
                content.AddContent(17, ItemFormatContent(SelectedItem));
            }
            else
            {
                if (DefaultItemContent != null)
                {
                    content.AddContent(17, DefaultItemContent);
                }
                else
                {
                    if(DataSource is null)
                    {
                        return;
                    }

                    var defaultItem = DataSource.ElementAtOrDefault(0);
                    if (defaultItem is null)
                    {
                        return;
                    }

                    SelectItem(defaultItem).GetAwaiter().GetResult();
                    content.AddContent(17, ItemFormatContent(defaultItem));
                }
            }
        }, 
        new { @class=HtmlHelper.CreateCssBuilder()
        .Append("default", DefaultItemContent != null && SelectedItem == null)
        .Append("filtered",Filtered)
        .Append("text").ToString() 
        });
    }

    /// <summary>
    /// Builds the drop down items.
    /// </summary>
    /// <param name="builder">The builder.</param>
    void BuildDropDownItems(RenderTreeBuilder builder, int sequence)
    {
        if (IsLoading)
        {
            return;
        }
        builder.CreateComponent<Menu>(sequence, content =>
        {
            var i = 0;
            if (FilterdDataSource != null)
            {
                foreach (var data in FilterdDataSource.Take(MaxItems))
                {
                    var index = i;

                    content.CreateComponent<Item>(0, item => item.AddContent(0, ItemFormatContent(data)),
                        new
                        {
                            AdditionalCssClass = HtmlHelper.CreateCssBuilder()
                            .Append("filtered", Filtered && !LocalFilter(FilterdInputValue, data))
                            .Append("active selected", SelectedItem != null && SelectedItem.Equals(data)).ToString(),
                            onclick = EventCallback.Factory.Create<MouseEventArgs>(this, () => SelectItem(data)),
                        }, appendFunc: (b, s) =>
                          {
                              b.SetKey(index);
                              return s;
                          });
                    i++;
                }
                content.CreateComponent<Message>(1, EmptyContent, condition: !FilterdDataSource.Any());
            }
        }, attributes: new
        {
            UI = false,
            AdditionalStyle = HtmlHelper.CreateStyleBuilder().Append("display:block", Active&&Span is null).Append("display:flex",Active && Span is not null).ToString(),
            AdditionalCssClass = HtmlHelper.CreateCssBuilder().Append(Active ? "visible" : "hidden").Append("scrollhint").ToString()
        });
    }

    private bool BuildLoadingContent(RenderTreeBuilder content,int sequence)
    {
        if (IsLoading)
        {
            content.CreateElement(sequence,"div", LoadingContent);
        }
        return IsLoading;
    }
#endregion

#region EventHandler
    /// <summary>
    /// Fetch data.
    /// </summary>
    /// <param name="value">The input value from search box.</param>
    async Task ReadData(string value)
    {
        IsLoading = true;
        if (DataSourceHandler is not null)
        {
            DataSource = await DataSourceHandler(value);
        }
        IsLoading = false;
    }

    /// <summary>
    /// Perform the toggle for actived.
    /// </summary>
    public async Task Toggle() => await this.Activate(!Active);


    /// <summary>
    /// Compares the search value.
    /// </summary>
    /// <param name="value">The value from search input.</param>
    /// <param name="item">The item of data source.</param>
    /// <returns></returns>
    private bool DefaultFilter(string? value, TItem item)
    {
        if (item is not string && typeof(TItem).IsClass)
        {
            var itemValue = GetItemValue(item);
            if (itemValue is null)
            {
                return false;
            }
            return itemValue.ToString().Contains(value);
        }
        return item.ToString().Contains(value);
    }

    /// <summary>
    /// Searches the specified input.
    /// </summary>
    /// <param name="value">The input value from search box.</param>
    async Task Search(string? value)
    {
        FilterdInputValue = value;
        await ReadData(value);

        Filtered = !string.IsNullOrEmpty(value);

        FilterdDataSource = Filtered ? DataSource.Where(item => LocalFilter(value, item)) : DataSource;

        StateHasChanged();
    }

    /// <summary>
    /// Selects the item.
    /// </summary>
    /// <param name="selectedItem">The selected item.</param>
    public async Task SelectItem(TItem selectedItem)
    {
        SelectedItem = selectedItem;
        Filtered = false;
        FilterdInputValue = default;
        //FilterdDataSource = DataSource;
        Value = GetItemValue(selectedItem);
        await ValueChanged.InvokeAsync(Value);
        await OnItemSelected.InvokeAsync(selectedItem);
    }

    private TValue? GetItemValue(TItem item) => (TValue?)item?.GetType()?.GetProperty(ValueField)?.GetValue(item);
    #endregion
}
